/**
 * @namespace Core utilities and functions.
 */
var Jelo = window.Jelo || {
    version : 1.090723
};

/**
 * Can be used to create unique IDs or global counters. Every time this function
 * is called, a number will be returned which is 1 greater than the number
 * previously returned.
 * 
 * @function
 * @return {Number} A unique (to this function), autoincrementing positive
 *         integer. The first number returned is 1.
 */
Jelo.uID = function() {
    var id = 1; // initial value
    return function() {
        return id++;
    };
}();

/**
 * A reusable no-operation function. Useful for placeholders.
 */
Jelo.emptyFn = function() {};

/**
 * Performs a function on each item in a collection. If the first argument is
 * not an array or object, the function is called once with it. If the function
 * ever returns false, execution halts immediately and the "failed" index is
 * returned. Normally, Jelo.each will return null.
 * 
 * @param {Array|NodeList|Object} collection The object over which to iterate.
 * @param {Function} fn The function to execute for each item in the collection.
 *        Arguments passed to the function will be the item, its index, and the
 *        complete array. For example, myFunc(collection[index], index,
 *        collection)
 * @param {Object} [scope] The scope ("this") in which to execute the function.
 *        By default, the scope will be the current item being iterated across.
 */
Jelo.each = function(a, f, s) {
    var i, ai;
    if (typeof a.length == "number") {
        for (i = 0, l = a.length; i < l; i++) {
            ai = a[i];
            if (typeof a[i] != 'undefined') {
                if (f.call(s || ai, ai, i, a) === false) {
                    return i;
                }
            }
        }
    } else if (typeof a == "object") {
        for (i in a) {
            ai = a[i];
            if (a.hasOwnProperty(i) && typeof ai != 'undefined') {
                if (f.call(s || ai, ai, i, a) === false) {
                    return i;
                }
            }
        }
    }
};

/**
 * Binds arguments to a function and optional scope.
 * 
 * @param {Function} fn The function to execute when the delegate is called.
 * @param {Object} [scope=window] The scope in which to execute the chosen
 *        function.
 * @param {Mixed} [...] Additional parameters to pass to the function
 * @return {Function} The new function reference.
 */
Jelo.delegate = function(fn, scope) {
    var s = scope || window;
    var a = [].slice.call(arguments, 2);
    return function() {
        fn.apply(s, a);
    };
};

// Normalizes window.console to prevent debug errors when there is no console.
(function() {
    if (!('console' in window) || !('firebug' in console)) {
        var names = ["log", "debug", "info", "warn", "error", "assert", "dir", "dirxml", "group", "groupEnd", "time",
            "timeEnd", "count", "trace", "profile", "profileEnd"];
        window.console = {};
        for (var i = 0; i < names.length; ++i) {
            // TODO: write to a hidden div, provide a method to toggle
            // visibility
            window.console[names[i]] = Jelo.emptyFn;
        }
    }
    /**
     * @namespace
     * @name Console
     * @memberOf Jelo
     */
    Jelo.Console = {
        firebug : window.console
    }; // Google likes to eat the console :(
    /**
     * @function
     * @param {Mixed} Information to log in the debug console.
     */
    Jelo.Console.log = window.console.log;
    /**
     * @function
     * @returns {Array} The current call stack.
     */
    Jelo.Console.getStackTrace = function() {
        var /* counter */i, /* length */len,
        /* stack */s = [],
        /* isStackPopulated */isPop = false, /* lines */l;
        try {
            hb.stone.javascript += 0; // raise an error
        } catch (e) {
            if (e.stack) {
                // Firefox
                l = e.stack.split("\n");
                for (i = 0, len = l.length; i < len; i++) {
                    if (l[i].match(/^\s*[a-z0-9\-_\$]+\(/i)) {
                        s.push(l[i]);
                    }
                }
                s.shift(); // remove call to getStackTrace
                isPop = true;
            } else if (window.opera && e.message) { // Opera
                l = e.message.split("\n");
                for (i = 0, len = l.length; i < len; i++) {
                    if (l[i].match(/^\s*[A-Za-z0-9\-_\$]+\(/)) {
                        var entry = l[i];
                        if (l[i + 1]) {
                            entry += " at " + l[i + 1]; // file info
                            i++;
                        }
                        s.push(entry);
                    }
                }
                s.shift(); // remove call to getStackTrace
                isPop = true;
            }
        }
        if (!isPop) {
            // IE and Safari
            var curr = arguments.callee;
            while ((curr = curr.caller)) {
                var fn = curr.toString();
                var fname = fn.substring(fn.indexOf("function") + 8, fn.indexOf("(")) || "anonymous";
                s.push(fname);
            }
        }
        return s;
        
    };
    
})();

/*
 * Normalizes setTimeout and setInterval behavior across all browsers.
 * Typically, IE does not treat additional arguments correctly, and Firefox adds
 * a "lateness" argument to the supplied function call.
 */
(function(f) {
    /**
     * @function
     * @name setTimeout
     * @param {Function} fn Method to invoke.
     * @param {Number} ms Milliseconds to delay before invoking fn.
     * @param {Mixed} [...] Additional arguments to be passed to fn when it is
     *        called.
     * @returns {Number} Resource id, can be cancelled using clearTimeout(id)
     */
    window.setTimeout = f(window.setTimeout);
    /**
     * @function
     * @name setInterval
     * @param {Function} fn Method to invoke.
     * @param {Number} ms Milliseconds to delay between intervals
     * @param {Mixed} [...] Additional arguments to be passed to fn when it is
     *        called.
     * @returns {Number} Resource id, can be cancelled using clearInterval(id)
     */
    window.setInterval = f(window.setInterval);
})(function(f) {
    return function(c, t) {
        var a = [].slice.call(arguments, 2);
        return f(function() {
            // TODO: what if string?????
            c.apply(this, a);
        }, t);
    };
});

// Normalizes Internet Explorer's behavior to match modern browsers.
(function() {
    if (typeof Array.prototype.indexOf == 'undefined') {
        /**
         * @memberOf Array
         * @param {Mixed} x The item to attempt to find.
         * @returns {Number} The item's index if found, -1 otherwise.
         */
        Array.prototype.indexOf = function(k) {
            var len = this.length;
            for (var i = 0; i < len; i++) {
                if (this[i] == k) {
                    return i;
                }
            }
            return -1;
        };
        Array.indexOf = Array.prototype.indexOf;
    }
    if (typeof Array.prototype.lastIndexOf == 'undefined') {
        /**
         * @memberOf Array
         * @param {Mixed} x The item to attempt to find.
         * @returns {Number} The index of the item's last occurrence if found,
         *          -1 otherwise.
         */
        Array.prototype.lastIndexOf = function(k) {
            var len = this.length;
            for (var i = len - 1; i > -1; i--) {
                if (this[i] == k) {
                    return i;
                }
            }
            return -1;
        };
        Array.lastIndexOf = Array.prototype.lastIndexOf;
    }
    if (typeof Array.prototype.find == 'undefined') {
        /**
         * @memberOf Array
         * @param {Mixed} x The item to attempt to find, or a RegExp to match.
         * @returns {Array|Boolean} An array of indeces at which the item was
         *          found, or at which the RegExp tested positive. Boolean false
         *          if no element matched.
         */
        Array.prototype.find = function(k) {
            var res = [];
            var len = this.length;
            for (var i = 0; i < len; i++) {
                if ((k.test && k.test(this[i])) || k === this[i]) {
                    res.push(i);
                }
            }
            return !!res.length && res;
        };
        Array.find = Array.prototype.find;
    }
    if (typeof Array.prototype.shuffle == 'undefined') {
        /**
         * @memberOf Array
         * @returns {Array} The array, randomized.
         */
        Array.prototype.shuffle = function() {
            for (var j, x, i = this.length; i; j = parseInt(Math.random() * i, 10), x = this[--i], this[i] = this[j], this[j] = x) {}
            return this;
        };
        Array.shuffle = Array.prototype.shuffle;
    }
})();

/**
 * Allows you to check or uncheck a group of checkboxes by clicking and dragging
 * across them (or their labels).
 * 
 * @function
 * @name dragCheckbox
 * @param {HTMLElement} [root=document] The element within which to apply "drag
 *        toggle" functionality.
 * @memberOf Jelo
 */
Jelo.dragCheckbox = function(root) {
    root = root || document;
    var dragging = false;
    var current = false;
    var undrag = function() {
        dragging = false;
    };
    Jelo.un(document, "mouseup", undrag);
    Jelo.on(document, "mouseup", undrag);
    var getTarget = function(element) {
        switch (element.tagName.toLowerCase()) {
            case 'input' :
                return element;
            case 'label' :
                Jelo.css(element, '-moz-user-select', 'none');
                Jelo.css(element, '-webkit-user-select', 'ignore');
                element.onselectstart = function() {
                    return false;
                };
                var el = element.getAttribute('for') || element.getAttribute('htmlFor');
                return $('input#' + el);
            default :
                return null; // invalid element
        }
    };
    Jelo.each($$('[type=checkbox]', root), function() {
        var down = function() {
            var box = getTarget(this);
            if (box) {
                dragging = true;
                box.checked = !box.checked;
                current = box.checked;
            }
        };
        var over = function() {
            var box = getTarget(this);
            if (box && dragging) {
                box.checked = current;
            }
        };
        var click = function(target, event) {
            var box = getTarget(this);
            if (box) {
                box.checked = current;
            }
        };
        Jelo.un(this, "mousedown", down);
        Jelo.un(this, "mouseover", over);
        Jelo.un(this, "click", click);
        Jelo.on(this, "mousedown", down);
        Jelo.on(this, "mouseover", over);
        Jelo.on(this, "click", click);
        var label = $('label[for=' + this.id + ']', root);
        if (label) {
            Jelo.un(label, "mousedown", down);
            Jelo.un(label, "mouseover", over);
            Jelo.un(label, "click", click);
            Jelo.on(label, "mousedown", down);
            Jelo.on(label, "mouseover", over);
            Jelo.on(label, "click", click);
        }
    });
};
